Skip to content

feat(governance): Pydantic AI adapter#359

Open
aditik0303 wants to merge 11 commits into
mainfrom
feat/governance-pydantic-ai
Open

feat(governance): Pydantic AI adapter#359
aditik0303 wants to merge 11 commits into
mainfrom
feat/governance-pydantic-ai

Conversation

@aditik0303

@aditik0303 aditik0303 commented Jun 24, 2026

Copy link
Copy Markdown

Summary

Adds a Pydantic AI governance adapter to uipath-pydantic-ai. It lets UiPath governance evaluate what a Pydantic AI Agent does at the model and tool level, and block disallowed actions, without the agent author writing governance code. This package contains only the Pydantic-AI-specific bridge.

What it does

  • Detects a pydantic_ai.Agent and governs it by wrapping agent.model with a GovernanceModel (a pydantic_ai WrapperModel). Both the non-streaming request and the streaming request_stream paths are bracketed.

  • Everything flows through the model, so governance hooks are derived from the request/response messages rather than from per-event callbacks:

    Pydantic AI signal Governance hook
    latest request message text (user prompt or tool result) BEFORE_MODEL
    model response text AFTER_MODEL
    each ToolCallPart the model emits TOOL_CALL
    each ToolReturnPart in the request AFTER_TOOL
  • Only the latest request message is scanned each turn, so a prompt or tool result is not re-evaluated every time the full history is re-sent for context.

  • Enforces by letting a GovernanceBlockException (raised on a DENY decision) propagate, stopping the model call or tool. Any other error inside a governance hook is logged and swallowed, so a governance failure cannot break an otherwise-healthy agent run.

  • Installed by the runtime factory: passing an evaluator to new_runtime wires governance onto the resolved agent in place. No adapter registry, no entry point, no import-time registration.

What it does not do

  • Does not fire agent-level boundaries (BEFORE_AGENT / AFTER_AGENT); those are owned by the governance host.
  • Does not depend on platform, auth, transport, or runtime internals — only on the shared governance contracts in uipath-core.

aditik0303 and others added 2 commits June 22, 2026 23:09
Wraps agent.model with a WrapperModel deriving all four hooks from message parts (UserPromptPart -> BEFORE_MODEL, TextPart -> AFTER_MODEL, ToolCallPart -> TOOL_CALL, ToolReturnPart -> AFTER_TOOL); covers request and request_stream.

Self-registers via the uipath.governance.adapters entry point; unit-tested and verified firing through the framework's real execution path. BEFORE/AFTER_AGENT remain owned by the uipath-runtime wrapper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, framework-only can_handle)

Mirror radu's LangChain-adapter review across the Pydantic AI adapter:
- __init__: drop the import-time registration side-effect; registration only via the uipath.governance.adapters entry point.
- can_handle: claim only a real pydantic_ai.Agent; remove the duck-typed (model/run/iter) fallback.
- docstring: 'governance host' instead of uipath-runtime internals.
- tests: a duck-typed look-alike is now rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 24, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a governance adapter for the uipath-pydantic-ai integration, enabling UiPath governance evaluators to observe and enforce policy around Pydantic AI model calls and tool usage via model-wrapping, with registration done through entry-point discovery.

Changes:

  • Introduces PydanticAIAdapter, GovernanceModel, and GovernanceCallbacks to bracket model requests/responses and tool events with governance hooks.
  • Adds an entry point (uipath.governance.adapters) to register the adapter without import-time mutation of the global registry.
  • Adds a focused unit test suite that uses real pydantic_ai types and rejects duck-typed look-alikes.

Reviewed changes

Copilot reviewed 4 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/uipath-pydantic-ai/src/uipath_pydantic_ai/governance/adapter.py Implements the Pydantic AI governance adapter and model wrapper that fires BEFORE_MODEL / AFTER_MODEL / TOOL_CALL / AFTER_TOOL.
packages/uipath-pydantic-ai/src/uipath_pydantic_ai/governance/init.py Provides an idempotent register_governance_adapter() used by entry-point discovery (no import-time registration).
packages/uipath-pydantic-ai/tests/governance/test_adapter.py Adds unit tests for can-handle behavior, attach/detach model wrapping, hook firing, and exception propagation/swallowing rules.
packages/uipath-pydantic-ai/tests/governance/init.py Adds the governance tests package marker.
packages/uipath-pydantic-ai/pyproject.toml Adds uipath-core dependency and a governance adapter entry point.
packages/uipath-pydantic-ai/uv.lock Locks the added uipath-core dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/uipath-pydantic-ai/src/uipath_pydantic_ai/governance/adapter.py Outdated
Comment thread packages/uipath-pydantic-ai/src/uipath_pydantic_ai/governance/adapter.py Outdated
aditik0303 and others added 9 commits June 24, 2026 18:26
…g path

Address Copilot review on the Pydantic AI adapter:
- request_stream's after-stream check caught all exceptions, swallowing GovernanceBlockException — a DENY during streaming did not abort the run, unlike the non-streaming request() path. Re-raise the block exception; keep swallowing other governance errors. Add a streaming block-propagation test.
- Module docstring: registers via the uipath.governance.adapters entry point, not at import time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These files were swept into the branch by a broad add; they are unrelated to the governance adapter. Reverting/removing them so the PR contains only governance changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An earlier cleanup commit compared against a stale local main and wrongly removed SETUP.MD and reverted the LlamaIndex docs change. Both files come from main (PRs #352/#356), not this branch. Restore them to the main version so this PR is governance-only with no spurious deletions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… message models

Replace the duck-typed `_part_kind` string-discriminator with isinstance
checks against pydantic_ai.messages public types (UserPromptPart,
ToolReturnPart, ToolCallPart, BuiltinToolCallPart, TextPart), matching
the typed-extraction pattern from the LangChain adapter review (#899).
Drops the `_part_kind` helper and the getattr fallbacks; attribute
access is now type-checked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Core PR #1761 removed BaseAdapter from uipath-core. Migrate to the
factory-evaluator pattern (matching #899):

- governance/adapter.py -> model.py: replace the BaseAdapter subclass
  (name/can_handle/attach/detach) with module-level install_governance();
  keep GovernanceModel + GovernanceCallbacks. File named for its seam
  (the model wrapper), like LangChain's callbacks.py.
- runtime/factory.py: new_runtime reads `evaluator` from kwargs and
  calls install_governance on the resolved agent.
- governance/__init__.py: drop register_governance_adapter + registry
  import; expose install_governance. No import-time side effects.
- pyproject.toml: remove the uipath.governance.adapters entry point.
- tests (test_adapter.py -> test_model.py): drop can_handle/attach/
  detach; cover install_governance + factory wiring.

ruff + mypy clean; 17 governance tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, tool_call_id

Review findings (Viswa) for PR #359:

- Streaming AFTER_MODEL now runs in a finally *inside* the request_stream
  async-with, so it fires even if the consumer's `async for` raises partway
  through, and while the stream context is still open. Documented that
  streaming governance is inherently post-hoc (tokens are already emitted by
  the time the response is complete; a DENY aborts the run but cannot un-send
  them).
- _latest_request now scans from the end for the last ModelRequest instead of
  blindly taking messages[-1] — a history ending in a ModelResponse (mid
  tool round-trip) would otherwise scan the wrong side of the exchange.
- Pass tool_call_id through to evaluate_tool_call / evaluate_after_tool so the
  evaluator can correlate a call with its result.
- Bounded joins (_join_within_cap) for request/response/content text and a
  capped _stringify, so an oversized tool-return can't build a multi-megabyte
  string before the final slice.
- Drop the per-callbacks uuid trace_id (identical for every call); trace
  correlation is owned by the layer below, matching LangChain. Requires
  uipath-core >= 0.5.20 (removed trace_id from EvaluatorProtocol) — bumped.
- Count llm/tool calls only after governance passes (no inflation on block).
- Documented that agent.model is a property whose setter the agent re-reads
  per run (no stale ref), and that a per-run model override bypasses the wrap.

Tests: added streaming happy-path (finalized response governed), streaming
finally-on-consumer-error, ModelRequest-filter, and tool_call_id passthrough.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the #359 review pass — the earlier commit covered the blockers
+ page-3 majors; these are the page-4 minors:

- model.py:239 — BuiltinToolReturnPart now fires AFTER_TOOL (symmetric with
  the built-in TOOL_CALL). A provider-executed built-in tool carries both its
  call and result inline in the response, so AFTER_TOOL for built-ins fires
  from on_response, not the next request.
- model.py:371 — _coerce_args preserves malformed JSON as {"_raw": args}
  instead of dropping it to {}, so an arg-based policy can still scan it (a
  malformed payload must not slip past governance).
- model.py:229 — tool_name fallback to "unknown" now logs a warning via a
  shared _tool_name() helper (used by all three tool-part call sites).

Not changed: runtime/factory.py:271 kwargs.get("evaluator") — this is the
same magic-string kwarg LangChain #899's factory uses (factory.py:281); it's
the host-dispatch contract shared by all adapters, so kept for parity.

Tests: added built-in-tool-result AFTER_TOOL coverage; updated _coerce_args
variant assertion. 82 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same pre-existing CI-lint failure as the other packages (mypy runs over tests):
- FakeEvaluator evaluate_* -> (self, *args, **kwargs) -> Any so it satisfies
  EvaluatorProtocol; bare dict -> dict[str, Any].
- corrected the FakeWrapped stub ignore ([attr-defined] -> [assignment]) and
  added arg-type ignores where None is passed for ModelRequestParameters in
  the request/request_stream bracket tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New-code coverage ~89% -> lift over the 90% gate. Added: non-block-error
swallow on the response/tool paths, on_request with only a ModelResponse
(empty-input branch), and _content_text/_stringify/_tool_name helper edges.
governance/model.py: 82% -> 96%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 1, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants